<?php
class Novel_service extends CI_Model
{

    const DEFAULT_COVER = 'http://image.hanlingsha.love/lwa/novel/lwa-default-novel-cover.jpeg';
    const DEFAULT_COUPON = 20;
    const BUY_COUPON_LOCK_PRE = 'buy_coupon_lock_%s';
    const DEFAULT_PAGE = 1;
    const DEFAULT_PAGE_SIZE = 20;

    public function __construct()
    {
        parent::__construct();
        $this->load->model('novel_model');
        $this->load->model('user_model');
        $this->load->model('tag_model');
        $this->load->model('chapter_model');
        $this->load->model('user_read_log_model');
        $this->load->model('consumption_log_model');
        $this->load->model('book_shelf_model');
        $this->load->model('read_log_offline_model');
        $this->load->library('redis_proxy');
    }

    /**
     * Get novel detail info
     *
     * @param string $novel_id
     * @param string $user_id
     * @param err $err
     * @return mixed
     */
    public function getNovelDetail($novel_id, $user_id, &$err)
    {
        if (empty($novel_id))
        {
            $err = [
                'errorCode'     => PARAM_ERR,
                'errorMessage'  => '参数异常',
            ];
            return false;
        }
        $novel_info = $this->novel_model->findOne([
            'fields'        => 'novel_id as novelId, 
            novel_name as novelName, cover_url as coverUrl, 
            tag_id as tagId, tag_name as tagName, 
            author_name as authorName, words_num as wordsNum, 
            chapter_num as chapterNum, is_done as isDone, intro, touch_num as touchNum',
            'where'         => [
                'novel_id'      => $novel_id,
                'hide'          => 'N',
                'deleted'       => 'N',
            ],
        ]);
        if (empty($novel_info))
        {
            $err = [
                'errorCode'     => NOVEL_NOT_FOUNE,
                'errorMessage'  => '书籍不存在',
            ];
            message_logger('debug', sprintf('novel [%s] not found', $novel_id));
            return false;
        }
        $novel_info['rate'] = mb_strlen($novel_info['intro']) % 5;
        $last_order_num = $novel_info['chapterNum'] == 0 ? 1 : $novel_info['chapterNum'];
        // show last chapter pushtime
        $novel_info['updateTime'] = $this->_findNovelUpdateTime($novel_id);
        $novel_info['collected'] = $this->_checkNovelCollected($novel_id, $user_id);
        $last_chapter_info = $this->chapter_model->findOne([
            'fields'    => 'chapter_name as chapterName, chapter_id as chapterId, novel_id as novelId, order_num as orderNum',
            'where'     => [
                'novel_id'      => $novel_id,
                'order_num'     => $last_order_num,
                'publish'       => 'Y',
                'deleted'       => 'N',
            ]
        ]);
        if (empty($last_chapter_info))
        {
            message_logger('debug', sprintf('not found chapter, novel_id [%s], order_num [%s]', $novel_id, $last_order_num));
        }
        // if the value is [], user not login or never read this book
        $last_read_chapter_info = [];
        if ($user_id)
        {
            $_get_last_read_err = null;
            $last_read_chapter_info = $this->getLastRead($user_id, $novel_id, $_get_last_read_err);
            if ( ! is_null($_get_last_read_err))
            {
                message_logger('error', sprintf('get last read error, [%s]', $_get_last_read_err['errorMessage']));
            }
        }
        $result = [
            'novelInfo'         => $novel_info,
            'lastChapter'       => $last_chapter_info,
            'lastRead'          => $last_read_chapter_info,
        ];
        $this->incrTouchNum($novel_id);
        return toCamelCaseKey($result);
    }

    public function getNovelLists($page, $page_size)
    {
        if (empty($page) || $page < 1)
        {
            $page = self::DEFAULT_PAGE;
        }
        if (empty($page_size) || $page_size > 100)
        {
            $page_size = self::DEFAULT_PAGE_SIZE;
        }
        $get_params = [
            'fields'        => 'novel_id, novel_name, is_done, cover_url, tag_id, tag_name, words_num, chapter_num',
            'where'         => [
                'deleted'       => 'N',
                'hide'          => 'N',
            ],
            'limit'         => $page_size,
            'offset'        => ($page - 1) * $page_size
        ];
        $novels = $this->novel_model->findAll($get_params);
        $total = $this->novel_model->findOne([
            'fields'        => 'count(*) as cnt',
            'where'         => [
                'deleted'       => 'N',
                'hide'          => 'N',
            ],
        ])['cnt'];
        $result = [
            'page'      => $page,
            'page_size' => $page_size,
            'novels'    => $novels,
            'total'     => $total,
        ];
        return toCamelCaseKey($result);
    }

    public function getChaptersList($novel_id, $user_id, &$err)
    {
        if (empty($novel_id))
        {
            $err = [
                'errorCode'     => PARAM_ERR,
                'errorMessage'  => '缺少参数',
            ];
            return false;
        }
        $chapter_list = $this->chapter_model->findAll([
            'fields'        => 'chapter_id, chapter_name, is_vip, order_num',
            'where'         => [
                'novel_id'  => $novel_id,
                'deleted'   => 'N',
                'publish'   => 'Y',
            ],
            'order_by'      => [
                'order_num' => 'asc',
            ],
        ]);
        if (empty($chapter_list))
        {
            return $chapter_list;
        }
        $this->_fillChapterList($chapter_list, $novel_id, $user_id);
        return toCamelCaseKey($chapter_list);
    }

    public function getChapterContent($novel_id, $order_num, $user_id, $is_pay, &$err)
    {
        list($lock, $err) = $this->redis_proxy->get(sprintf(self::BUY_COUPON_LOCK_PRE, $user_id));
        if ( ! is_null($err))
        {
            $err = [
                'errorCode'     => SYSTEM_ERR,
                'errorMessage'  => '系统异常',
            ];
            message_logger('error', sprintf('redis error [%s]', $err));
            return false;
        }
        if ($lock == 'Y')
        {
            $err = [
                'errorCode'     => SYSTEM_ERR,
                'errorMessage'  => '请求过于频繁',
            ];
            return false;
        }
        $chapter = $this->chapter_model->findOne([
            'fields'        => 'novel_id, order_num, content, is_vip, chapter_id, coupon, word_num, chapter_name',
            'where'         => [
                'deleted'       => 'N',
                'publish'       => 'Y',
                'novel_id'      => $novel_id,
                'order_num'     => $order_num,
            ],
        ]);
        if (empty($chapter))
        {
            $err = [
                'errorCode'     => CHAPTER_NOT_FOUND,
                'errorMessage'  => '章节不存在',
            ];
            message_logger('error', sprintf('chapter not found, novel [%s], order_num [%s]', $novel_id, $order_num));
            return false;
        }
        $novel_name = $this->novel_model->findOne([
            'fields'    => 'novel_name',
            'where'     => [
                'novel_id'  => $novel_id,
            ]
        ])['novel_name'];
        $is_vip = $chapter['is_vip'];
        $user_coupon = $this->user_model->findOne([
            'fields'        => 'coupon',
            'where'         => [
                'user_id'       => $user_id,
            ],
        ])['coupon'];
        $result = [
            'content'       => '',
            'userCoupon'    => $user_coupon,
            'chapterCoupon' => 0,
            'isBought'      => 'N',
            'isVip'         => $is_vip,
            'chapterName'   => $chapter['chapter_name'],
            'novelName'     => $novel_name,
        ];

        if ($is_vip == 'Y')
        {
            if ( is_null($user_id))
            {
                $err = [
                    'errorCode'     => NO_LOGIN,
                    'errorMessage'  => '付费章节未登录',
                ];
                return false;
            }
            // check buougt
            if ($this->_checkBought($user_id, $novel_id, $chapter['chapter_id']))
            {
                $result['content'] = $chapter['content'];
                $result['isBought'] = 'Y';
                return $result;
            }
            $chapter_coupon = $chapter['coupon'];
            $novel_coupon = $this->novel_model->findOne([
                'fields'        => 'coupon',
                'where'         => [
                    'novel_id'  => $novel_id,
                ],
            ])['coupon'];
            $real_coupon = ceil($this->_getChapterRealCoupon($novel_coupon, $chapter_coupon, $chapter['word_num']));
            $result['chapterCoupon'] = $real_coupon;
            if ($is_pay == 'Y')
            {
                if ($user_coupon < $real_coupon)
                {
                    $err = [
                        'errorCode'     => NOT_COUPON,
                        'errorMessage'  => '书券不足',
                    ];
                    message_logger('debug', sprintf('user [%s] coupon [%s], chapter coupon [%s] ', $user_id, $user_coupon, $real_coupon));
                    return false;
                }
                $this->redis_proxy->set(sprintf(self::BUY_COUPON_LOCK_PRE, $user_id), 'Y');
                $this->_decrUserCoupon($user_id, $real_coupon);
                $this->_recordReadLog($user_id, $novel_id, $chapter['chapter_id']);
                $this->_recorConsumptionLog($user_id, $novel_id, $chapter['chapter_id'], $real_coupon);
                $this->_recordReadLogOffline($user_id, $novel_id, $chapter['chapter_id'], $order_num);
                $this->redis_proxy->del(sprintf(self::BUY_COUPON_LOCK_PRE, $user_id));
                $result['content'] = $chapter['content'];
                $result['isBought'] = 'Y';
            }
            else
            {
                $result['content'] = mb_substr($chapter['content'], 0, 50) . '...';
            }
        }
        else
        {
            if ( ! is_null($user_id))
            {
                $this->_recordReadLog($user_id, $novel_id, $chapter['chapter_id']);
                $this->_recordReadLogOffline($user_id, $novel_id, $chapter['chapter_id'], $order_num);
            }
            $result['content'] = $chapter['content'];
        }
        return $result;
    }

    public function createNovel($novel_info, &$err)
    {
        $must_column = [
            'novel_name', 'is_done', 'author_id',
            'tag_id', 'intro',
        ];
        if ( ! isset($novel_info['cover_url']) || empty($novel_info['cover_info']))
        {
            $novel_info['cover_url'] = self::DEFAULT_COVER;
        }
        $insert_data = [];
        foreach($must_column as $key => $value)
        {
            if ( ! isset($novel_info[$value]) || empty($novel_info[$value]))
            {
                $err = [
                    'errorCode'     => PARAM_ERR,
                    'errorMessage'  => '缺少参数',
                ];
                return false;
            }
            $insert_data[$value] = $novel_info[$value];
        }
        if (isset($novel_info['publish']) && $novel_info['publish'] == 'Y')
        {
            $insert_data['publish'] = 'Y';
        }
        $insert_data['author_name'] = $this->_findUserNameById($insert_data['author_id']);
        $insert_data['tag_name'] = $this->_findTagName($insert_data['tag_id']);
        $insert_data['add_time'] = time();
        $novel_id = $this->novel_model->getUuid();
        $insert_data['novel_id'] = $novel_id;
        $this->novel_model->insert($insert_data);
        return true;
    }

    public function searchNovels($search, &$err)
    {
        if (empty($search)) {
            $err = [
                'errorCode'     => PARAM_ERR,
                'errorMessage'  => '缺少参数',
            ];
            return false;
        }
        $novels = $this->novel_model->findAll([
            'fields' => 'novel_name, novel_id, cover_url, tag_name, author_name',
            'like' => [
                'novel_name' => $search,
            ]
        ]);

        return toCamelCaseKey($novels);
    }

    public function getAllTag()
    {
        $tags = $this->tag_model->findAll([
            'fields'        => 'tag_id, tag_name',
            'where'         => [
                'deleted'   => 'N',
            ]
        ]);
        return $tags;
    }

    public function initTag()
    {
        $tags = [
            '都市言情', '古代言情', '青春校园', '悬疑惊悚', '热血青春', '魔幻异界',
            '仙侠情缘', '穿越重生', '都市异能', '都市娱乐', '都市情感', '游戏竞技'
        ];
        array_walk($tags, function($tag_name){
            $tag_id = $this->tag_model->getUuid();
            $insert_data = [
                'tag_id'        => $tag_id,
                'tag_name'      => $tag_name,
            ];
            $this->tag_model->insert($insert_data);
        });
    }

    public function hideNovel($novel_id, $hide = true)
    {

    }

    public function updateNovelInfo($novel_id, $update_info, &$err)
    {

    }

    public function getRandomNovels()
    {
        $sql = 'select novel_id, cover_url, novel_name from novels where deleted = ? and hide = ? and publish = ? order by rand() limit ?';
        $result = $this->novel_model->db->query($sql, ['N', 'N', 'Y', 4])->result_array();
        return toCamelCaseKey($result);
    }

    /**
     * Get user last read chapter
     * If novel_id not null , search by novel_id
     *
     * @param string $user_id
     * @param string $novel_id
     * @param err $err
     * @return mixed
     */
    public function getLastRead($user_id, $novel_id, &$err)
    {
        $read_log = $this->user_read_log_model->findOne([
            'fields'        => 'novel_id, chapter_id',
            'where'         => [
                'user_id'       => $user_id,
                'novel_id'      => $novel_id,
            ],
            'limit'         => 1,
            'order_by'      => [
                'read_time'     => 'desc',
            ]
        ]);
        if (empty($read_log))
        {
            return [];
        }
        $order_num = $this->chapter_model->findOne([
            'fields'    => 'order_num',
            'where'     => [
                'chapter_id'        => $read_log['chapter_id'],
                'novel_id'          => $novel_id,
            ]
        ])['order_num'] ?? 0;
        if ($order_num == 0)
        {
            message_logger('error', sprintf('novel [%s] chapter [%s] not found order num' , $novel_id, $read_log['chapter_id']));
        }
        $read_log['order_num'] = $order_num;
        return $read_log;
    }

    /**
     * incr a novel hits number
     *
     * @param string $novel_id
     * @return void
     */
    public function incrTouchNum($novel_id)
    {
        $this->user_model->db->trans_start();
        $this->user_model->db->query('update novels set touch_num = touch_num + 1 where novel_id = ?', [$novel_id]);
        $this->user_model->db->trans_complete();
    }

    public function createChapter($chapter_info, $user_id, $novel_id, &$err)
    {
        $must_column = [
            'content', 'is_vip', 'publish', '', 'chapter_name'
        ];
        foreach($must_column as $key => $value)
        {
            if ( ! isset($chapter_info[$value]) || empty($chapter_info[$value]))
            {
                $err = [
                    'errorCode'     => PARAM_ERR,
                    'errorMessage'  => '参数错误',
                ];
                return false;
            }
        }
        $last_order_num = $this->_findLastOrderNum($novel_id);
        $order_num = $last_order_num + 1;
        $word_num = mb_strlen($chapter_info['content']);
        $this->chapter_model->insert([
            'chapter_id'        => $this->chapter_model->getUuid(),
            'chapter_name'      => $chapter_info['chapter_name'],
            'is_vip'            => $chapter_info['is_vip'] == 'Y' ? 'Y' : 'N',
            'publish'           => $chapter_info['publish'] == 'Y' ? 'Y' : 'N',
            'publish_time'      => is_numeric($chapter_info['publish_time']) ?? 0,
            'add_time'          => time(),
            'order_num'         => $order_num,
            'novel_id'          => $novel_id,
            'content'           => $chapter_info['content'],
        ]);
        return true;
    }

    public function getReadLog($user_id, &$err)
    {
        if (empty($user_id))
        {
            $err = [
                'errorCode'     => PARAM_ERR,
                'errorMessage'  => '参数错误',
            ];
            return false;
        }
        $lists = $this->read_log_offline_model->findAll([
            'fields'        => 'user_id, novel_id, order_num, read_time, chapter_id',
            'where'         => [
                'user_id'   => $user_id,
            ],
            'limit'         => 50,
            'order_by'      => [
                'read_time'     => 'desc',
            ]
        ]);
        if (empty($lists))
        {
            return [];
        }
        $this->_fillNovel($lists);
        $this->_fillChapter($lists);
        return toCamelCaseKey($lists);
    }

    private function _fillChapter(&$lists)
    {
        $chapter_ids = array_column($lists, 'chapter_id');
        $chapters = $this->chapter_model->findAll([
            'fields'        => 'chapter_id, chapter_name',
            'where_in'      => [
                'chapter_id'        => $chapter_ids,
            ]
        ]);
        $chapter_map = array_column($chapters, null, 'chapter_id');
        array_walk($lists, function(&$row) use ($chapter_map) {
            $chapter_id = $row['chapter_id'];
            $row['chapter_name'] = $chapter_map[$chapter_id]['chapter_name'] ?? '';
        });
    }

    private function _fillNovel(&$lists)
    {
        $novel_ids = array_column($lists, 'novel_id');
        $novels = $this->novel_model->findAll([
            'fields'            => 'novel_id, novel_name, tag_name, is_done, cover_url',
            'where_in'          => [
                'novel_id'  => $novel_ids,
            ]
        ]);
        $novel_map = array_column($novels, null, 'novel_id');
        array_walk($lists, function(&$row) use ($novel_map) {
            $novel_id = $row['novel_id'];
            $row['novel_name'] = $novel_map[$novel_id]['novel_name'] ?? '';
            $row['tag_name'] = $novel_map[$novel_id]['tag_name'] ?? '';
            $row['is_done'] = $novel_map[$novel_id]['is_done'] ?? '';
            $row['cover_url'] = $novel_map[$novel_id]['cover_url'] ?? '';
        });
    }

    private function _findNovelUpdateTime($novel_id)
    {
        $chapter_add_time = $this->chapter_model->findOne([
            'fields'    => 'add_time',
            'where'     => [
                'novel_id'  => $novel_id,
            ],
            'limit'     => 1,
            'order_by'  => [
                'order_num' => 'desc',
            ]
        ])['add_time'] ?? time();
        return $chapter_add_time;
    }

    private function _findLastOrderNum($novel_id)
    {
        $row = $this->chapter_model->findOne([
            'fields'    => 'order_num',
            'where'     => [
                'novel_id'      => $novel_id,
                'deleted'       => 'N',
            ],
            'order_by'  => [
                'order_num' => 'desc',
            ],
        ]);
        return $row['order_num'] ?? 0;
    }

    private function _fillChapterList(&$chapter_list, $novel_id, $user_id)
    {
        $read_log = [];
        if ( ! empty($user_id))
        {
            $read_log = $this->user_read_log_model->findAll([
                'fields'        => 'user_id, novel_id, chapter_id',
                'where'         => [
                    'user_id'   => $user_id,
                    'novel_id'  => $novel_id,
                ],
            ]);
        }
        
        $consumption_log = $this->consumption_log_model->findAll([
            'fields'        => 'novel_id, user_id, chapter_id',
            'where'         => [
                'user_id'   => $user_id,
                'novel_id'  => $novel_id,
            ],
        ]);
        $read_map = array_column($read_log, null, 'chapter_id');
        $consumption_map = array_column($consumption_log, null, 'chapter_id');
        array_walk($chapter_list, function(&$row) use ($read_map, $consumption_map) {
            $row['readed'] = isset($read_map[$row['chapter_id']]) ? 'Y' : 'N';
            $row['bought'] = isset($consumption_map[$row['chapter_id']]) ? 'Y' : 'N';
        });
    }

    private function _decrUserCoupon($user_id, $coupon)
    {
        $this->user_model->db->trans_start();
        $this->user_model->db->query('update users set coupon = coupon - ? where user_id = ?', [$coupon, $user_id]);
        $this->user_model->db->trans_complete();
    }

    private function _recordReadLog($user_id, $novel_id, $chapter_id)
    {
        if ($this->_checkReadLogExist($user_id, $novel_id, $chapter_id))
        {
            return null;
        }
        $log_id = $this->user_read_log_model->getUuid();
        $this->user_read_log_model->insert([
            'read_log_id'       => $log_id,
            'novel_id'          => $novel_id,
            'chapter_id'        => $chapter_id,
            'user_id'           => $user_id,
            'read_time'         => time(),
        ]);
    }

    private function _checkReadLogExist($user_id, $novel_id, $chapter_id)
    {
        $res = $this->user_read_log_model->findOne([
            'fields'    => 'read_log_id',
            'where' => [
                'novel_id'          => $novel_id,
                'chapter_id'        => $chapter_id,
                'user_id'           => $user_id,
            ]
        ]);
        return ! empty($res);
    }

    private function _recorConsumptionLog($user_id, $novel_id, $chapter_id, $coupon)
    {
        $consumption_log_id = $this->consumption_log_model->getUuid();
        $this->consumption_log_model->insert([
            'consumption_log_id'    => $consumption_log_id,
            'user_id'               => $user_id,
            'chapter_id'            => $chapter_id,
            'coupon'                => $coupon,
            'novel_id'              => $novel_id,
            'add_time'              => time(),
        ]);
    }

    private function _getChapterRealCoupon($novel_coupon, $chapter_coupon, $word_num)
    {
        if ($chapter_coupon > 0)
        {
            return $chapter_coupon;
        }
        if ($novel_coupon > 0)
        {
            $real_coupon = ($word_num / 1000) * $novel_coupon;
            return $real_coupon;
        }
        $real_coupon = ($word_num / 1000) * self::DEFAULT_COUPON;
        return $real_coupon;
    }

    private function _checkBought($user_id, $novel_id, $chapter_id)
    {
        $row = $this->consumption_log_model->findOne([
            'fields'    => 'consumption_log_id',
            'where'     => [
                'user_id'   => $user_id,
                'novel_id'  => $novel_id,
                'chapter_id'=> $chapter_id,
            ],
        ]);
        if (empty($row))
        {
            return false;
        }
        return true;
    }

    private function _findTagName($tag_id)
    {
        $tag_name = $this->tag_model->findOne([
            'fields'    => 'tag_name',
            'where'     => [
                'tag_id'        => $tag_id,
                'deleted'       => 'N',
            ],
        ])['tag_name'] ?? null;
        return $tag_name;
    }

    private function _findUserNameById($user_id)
    {
        $user_name = $this->user_model->findOne([
            'fields'    => 'user_name',
            'where'     => [
                'user_id'  => $user_id,
            ],
        ])['user_name'] ?? null;
        return $user_name;
    }

    private function _checkNovelCollected($novel_id, $user_id)
    {
        if (empty($user_id))
        {
            return false;
        }
        $row = $this->book_shelf_model->findOne([
            'fields'        => 'shelf_id',
            'where'         => [
                'novel_id'      => $novel_id,
                'user_id'       => $user_id,
            ]
        ]);
        if (empty($row))
        {
            return false;
        }
        return true;
    }

    private function _recordReadLogOffline($user_id, $novel_id, $chapter_id, $order_num)
    {
        $row = $this->read_log_offline_model->findOne([
            'fields'        => 'read_log_offline_id',
            'where'         => [
                'user_id'   => $user_id,
                'novel_id'  => $novel_id,
            ]
        ]);
        if (empty($row))
        {
            $read_log_offline_id = $this->read_log_offline_model->getUuid();
            $this->read_log_offline_model->insert([
                'read_log_offline_id'       => $read_log_offline_id,
                'user_id'                   => $user_id,
                'novel_id'                  => $novel_id,
                'chapter_id'                => $chapter_id,
                'order_num'                 => $order_num,
                'read_time'                 => time(),
            ]);
            return true;
        }
        $read_log_offline_id = $row['read_log_offline_id'];
        $update_where = [
            'where'     => [
                'read_log_offline_id'   => $read_log_offline_id,
                'user_id'               => $user_id,
                'novel_id'              => $novel_id,
            ]
        ];
        $update_info = [
            'read_time'     => time(),
            'chapter_id'    => $chapter_id,
            'order_num'     => $order_num,
        ];
        $this->read_log_offline_model->update($update_where, $update_info);
        return true;
    }
}
